#!/usr/bin/env python3
# Copyright (c) 2019 Red Hat, Inc. All rights reserved. This copyrighted
# material is made available to anyone wishing to use, modify, copy, or
# redistribute it subject to the terms and conditions of the GNU General
# Public License v.2 or later.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Script using koji to download kernel-headers for kernel."""

import argparse
import json
import os
import pprint
import re
import shlex
import subprocess
import time

from cki_lib import misc
from cki_lib.logger import get_logger
from cki_lib.misc import safe_popen
import sentry_sdk

LOGGER = get_logger('cki.cki_tools.get_kernel_headers')


class KojiWrap:
    """Wrap koji utility to download taskids and correlate info from webui."""

    def __init__(self, web_url, server_url, top_url):
        """Initialize with URLs."""
        self.web_url = web_url
        self.server_url = server_url
        self.top_url = top_url

    def run_cmd(self, cmd):
        """Run shell command 'koji' with server params set."""
        cmd = f"""koji --server {self.server_url} """ + \
            f"""--weburl {self.web_url} --topurl {self.top_url} --noauth {cmd}"""

        stdout, stderr, returncode = safe_popen(shlex.split(cmd),
                                                stdout=subprocess.PIPE,
                                                stderr=subprocess.PIPE)

        return stdout, stderr, returncode

    def list_builds(self, **kwargs):
        """Run 'koji list-builds' and pass arguments (--key=value) to it."""
        cmd = 'list-builds --reverse'
        for key, value in kwargs.items():
            cmd += f' --{key}={value}'

        stdout, stderr, returncode = self.run_cmd(cmd)
        if returncode:
            LOGGER.error('Koji call %s failed', cmd)
            LOGGER.error('stdout: %s',  stdout)
            LOGGER.error('stderr: %s',  stderr)
            raise RuntimeError(f'error: koji retcode=={returncode}')

        return stdout

    def koji_correlate_headers(self, nvr, exact=True):
        """Get task info for a given kernel taskid, find kernel-headers build.

        Arguments:
            nvr:                str, the kernel-headers build to download
            exact:              bool, take only exact headers version
        Returns:
            a list of build nvrs (str) for kernel-headers or []
        """
        # pull specific build/owner to help us look for a related koji package
        nvr = nvr.replace('kernel-', '')
        release = re.fullmatch(r'.*?\.((fc|eln)[0-9]*).*?', nvr).group(1)

        # find kernel-headers with nvr exactly matching kernel, or those that
        # were built before kernel
        alt_regex = fr'^(kernel-headers-.*?\.{release})\s+([^\n]+)\s+([^\n]+)'
        regex = fr'^(kernel-headers-{re.escape(nvr)})\s+([^\n]+)\s+([^\n]+)' \
            if exact else alt_regex

        stdout = self.list_builds(state='COMPLETE', package='kernel-headers')
        results = []
        for line in stdout.splitlines():
            match_result = re.match(regex, line)
            if match_result:
                results.append(match_result.group(1))

        return results

    def nvr(self, taskid: int) -> str:
        """Get the NVR of single taskid."""
        stdout, _, _ = self.run_cmd(f'call --json-output listBuilds taskID={taskid}')
        return json.loads(stdout)[0]['nvr']

    def download_build(self, nvr, max_retries=20, **kwargs):
        """Download build (by NVR) using 'koji download-build', allow retries.

        Try to download koji build, sleep 15s on failure, allow at most
        max_retries.

        Arguments:
            nvr:     str, nvr of the build to download
            max_retries: int, how many times to retry on failure, default: 20

            kwargs:
                arch: str, e.g. 'x86_64', the arch to download task for
        """
        LOGGER.info("📥 Downloading packages from Koji/Brew...")
        # prepare failure message
        fail_msg = f"Giving up after {max_retries} tries"

        cmd = 'download-build --noprogress '
        for key, value in kwargs.items():
            cmd += f'--{key}={value} '

        cmd += '--arch=src ' + nvr

        success = False
        while max_retries:
            stdout, stderr, returncode = self.run_cmd(cmd)
            success = returncode == 0
            if not success:
                LOGGER.debug(stdout)
                LOGGER.debug(stderr)
                if 'Forbidden' in stderr:
                    return False

            if success:
                return True

            time.sleep(15)
            max_retries -= 1

        if not success:
            LOGGER.info(fail_msg)

        return False


def go_download(arch, dst_dir, server_url, web_url, top_url, taskid):
    # pylint: disable=too-many-arguments,too-many-positional-arguments
    """Download kernel-headers related to kernel to dst_dir.

    Arguments:
        arch:       str, e.g. 'x86_64', the arch to download task for
        taskid:     int, taskid to find kernel-headers for
        dst_dir:    str, a directory where to download the files
        server_url: str, koji server_url param
        web_url:    str, koji web_url param
        top_url:    str, koji top_url param
    """
    koji_wrap = KojiWrap(web_url, server_url, top_url)

    kernel_nvr = koji_wrap.nvr(taskid)

    related_nvrs = koji_wrap.koji_correlate_headers(kernel_nvr, exact=True)
    if not related_nvrs:
        related_nvrs = koji_wrap.koji_correlate_headers(kernel_nvr,
                                                        exact=False)

    if not related_nvrs:
        raise RuntimeError('Failed to find kernel-headers task!')

    start_dir = os.getcwd()
    try:
        # download task to dst_dir
        os.chdir(dst_dir)

        LOGGER.debug(
            'The list of NVRs we got is: %s', pprint.pformat(related_nvrs)
        )
        for temp_nvr in related_nvrs:
            if koji_wrap.download_build(temp_nvr, arch=arch):
                break
    except:  # noqa: E722
        # in any case, return to directory we started in
        os.chdir(start_dir)
        # re-raise exception
        raise
    # in any case, return to directory we started in
    os.chdir(start_dir)


def main(args: list[str] | None = None) -> None:
    # pylint: disable=unbalanced-tuple-unpacking
    """Get params and complete go_download."""
    parser = argparse.ArgumentParser()
    parser.add_argument('arch', help='Architecture')
    parser.add_argument('taskid', help='Koji task ID')
    parser.add_argument('dst_dir', help='Destination directory')
    parsed_args = parser.parse_args(args)

    server_url = os.environ['server_url']
    web_url = os.environ['web_url']
    top_url = os.environ['top_url']

    go_download(parsed_args.arch, parsed_args.dst_dir, server_url, web_url, top_url,
                parsed_args.taskid)


if __name__ == '__main__':
    misc.sentry_init(sentry_sdk)
    main()
